/*--------------------------------------------------------------------------
 *  Copyright 2009 Taro L. Saito
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *--------------------------------------------------------------------------*/
//--------------------------------------
// XerialJ
//
// SilkParser.java
// Since: Apr 23, 2009 10:36:56 AM
//
// $URL$
// $Author$
//--------------------------------------
package org.xerial.silk.parser;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;

import org.xerial.core.XerialError;
import org.xerial.core.XerialErrorCode;
import org.xerial.core.XerialException;
import org.xerial.silk.SilkErrorCode;
import org.xerial.silk.SilkException;
import org.xerial.silk.model.SilkBlankLine;
import org.xerial.silk.model.SilkCommentLine;
import org.xerial.silk.model.SilkDataLine;
import org.xerial.silk.model.SilkElement;
import org.xerial.silk.model.SilkFunction;
import org.xerial.silk.model.SilkNode.SilkNodeBuilder;
import org.xerial.silk.model.SilkNodeModifier;
import org.xerial.silk.model.SilkNodeType;
import org.xerial.silk.model.SilkPreamble;
import org.xerial.silk.parser.token.SilkIndentToken;
import org.xerial.silk.parser.token.SilkToken;
import org.xerial.silk.parser.token.SilkTokenType;
import org.xerial.util.StringUtil;

/**
 * Push-style Silk Parser
 * 
 * @author leo
 * 
 */
public class SilkParser
{
    private final SilkLexer          lexer;
    private SilkElementHandler       handler        = null;

    private final SilkEnv            env;

    private static final SilkToken   EOF_TOKEN      = new SilkToken(SilkTokenType.EOF, -1);
    private static final SilkElement BlankLineEvent = new SilkBlankLine();

    public SilkParser(URL resourceURL) throws IOException {
        this(resourceURL.openStream());
    }

    public SilkParser(URL resourceURL, SilkEnv env) throws IOException {
        this(new SilkLexer(resourceURL.openStream()), env);
    }

    public SilkParser(Reader reader) {
        this(new SilkLexer(reader), SilkEnv.newEnv());
    }

    public SilkParser(InputStream in) {
        this(new SilkLexer(in), SilkEnv.newEnv());
    }

    protected SilkParser(SilkLexer lexer, SilkEnv env) {
        this.lexer = lexer;
        this.env = env;
    }

    public void close() throws IOException {
        lexer.close();
    }

    protected SilkToken LA(int k) throws XerialException {
        SilkToken t = lexer.LA(k);
        return t != null ? t : EOF_TOKEN;
    }

    protected SilkToken consume() throws XerialException {
        return lexer.next();
    }

    /**
     * Parse the input Silk file, then report the found SilkEvents to the given
     * handler
     * 
     * @param handler
     * @throws Exception
     */
    public void parse(SilkElementHandler handler) throws Exception {
        if (handler == null)
            throw new XerialError(XerialErrorCode.INVALID_INPUT, "null handler");

        this.handler = handler;

        outer_loop: for (SilkToken token; (token = LA(1)) != null;) {
            switch (token.type) {
            case Indent: {
                consume();
                SilkIndentToken indent = token.cast();
                parseInit(env.getIndentationOffset() + indent.indentLength);
                break;
            }
            case EOF:
                consume();
                break outer_loop;
            default:
                parseInit(env.getIndentationOffset());
            }
        }

    }

    private void emit(SilkElement e) throws Exception {
        handler.handle(e);
    }

    private void emit(SilkNodeBuilder node) throws Exception {
        handler.handle(node.build());
    }

    void parseInit(int indent) throws Exception {

        SilkToken t = lexer.LA(1);
        switch (t.type) {
        case DataLine:
            consume();
            emit(new SilkDataLine(indent, t.getText()));
            break;
        case Node:
            consume();
            emit(parseNode(SilkNodeType.SINGLE, indent));
            break;
        case TabNode:
            consume();
            emit(parseNode(SilkNodeType.TAB, indent));
            break;
        case SeqNode:
            consume();
            emit(parseNode(SilkNodeType.SEQUENCE, indent));
            break;
        case BlockNode:
            consume();
            emit(parseNode(SilkNodeType.BLOCK, indent));
            break;
        case HereDocSep:
            consume();
            emit(parseNode(SilkNodeType.HEREDOC, indent));
            break;
        case At:
            emit(parseFunction(indent));
            break;
        case Preamble:
            emit(parsePreamble(indent));
            break;
        case BlankLine:
            consume();
            emit(BlankLineEvent);
            break;
        case LineComment:
            consume();
            emit(new SilkCommentLine(t.getText()));
            break;
        case HereDoc:
            consume();
            emit(new SilkDataLine(indent, t.getText()));
            break;
        case EOF:
            return;
        case Error:
        default:
            throw new SilkException(SilkErrorCode.INVALID_INPUT, t.toString());
        }
    }

    private SilkPreamble parsePreamble(int indentLength) throws XerialException {
        testAndConsume(SilkTokenType.Preamble);
        SilkNodeBuilder preamble = parseNode();
        preamble.setIndent(indentLength);

        return preamble.buildPreamble();
    }

    private SilkFunction parseFunction(int indentLength) throws XerialException {
        testAndConsume(SilkTokenType.At);

        // <function name> ('(' <args> ')' | '-' <args>)
        SilkNodeBuilder func = new SilkNodeBuilder();
        func.setIndent(indentLength);
        SilkToken funcName = testAndConsume(SilkTokenType.QName);
        func.setName(funcName.getText());

        SilkToken t = LA(1);
        switch (t.type) {
        case LParen:
            // '(' <args> ')'
            consume();
            parseFunctionArgs(func);
            testAndConsume(SilkTokenType.RParen);
            break;
        case Separator:
            // '-' <args> 
            consume();
            parseFunctionArgs(func);
            break;
        }

        //parseNewLine();
        return func.buildFunction();
    }

    private void parseFunctionArgs(SilkNodeBuilder func) throws XerialException {
        // ( <arg> (',' <arg>)* )*
        parseFunctionArgSingle(func);

        while (nextTokenIs(SilkTokenType.Comma)) {
            consume();
            parseFunctionArgSingle(func);
        }
    }

    private void parseFunctionArgSingle(SilkNodeBuilder func) throws XerialException {
        SilkToken t = LA(1);
        switch (t.type) {
        case String:
        case QName:
        case Integer:
        case Real:
            consume();
            {
                SilkToken t2 = LA(1);
                switch (t2.type) {
                case Colon: {
                    consume();
                    SilkToken t3 = LA(1);
                    switch (t3.type) {
                    case String:
                    case QName:
                    case Integer:
                    case Real:
                        consume();
                        func.addSilkNode(new SilkNodeBuilder().setName(t.getText()).setValue(t3.getText()));
                        break;
                    default:
                        return;
                    }
                    break;
                }
                default:
                    func.addSilkNode(new SilkNodeBuilder().setValue(t.getText()));
                    break;
                }
            }
            break;
        }
    }

    private SilkNodeBuilder parseNode(SilkNodeType format, int indentLength) throws XerialException {
        SilkNodeBuilder node = new SilkNodeBuilder();
        node.setIndent(indentLength);
        node.setNodeType(format);

        return parseNode(node);
    }

    private SilkNodeBuilder parseNode() throws XerialException {
        return parseNode(new SilkNodeBuilder());
    }

    private SilkNodeBuilder parseNode(SilkNodeBuilder node) throws XerialException {
        // <node name>? <node type>? <attributes>? <node value>
        parseNodeName(node);
        parseNodeType(node);
        parseAttributes(node);
        parseNodeModifier(node);
        parseNodeValue(node);
        //parseNewLine();
        return node;
    }

    private void parseNodeModifier(SilkNodeBuilder node) throws XerialException {
        SilkToken t = LA(1);
        switch (t.type) {
        case Asterisk:
            consume();
            node.setModifier(SilkNodeModifier.ZERO_OR_MORE);
            break;
        case Question:
            consume();
            node.setModifier(SilkNodeModifier.ONE_OR_ZERO);
            break;
        case Plus:
            consume();
            node.setModifier(SilkNodeModifier.ONE_OR_MORE);
            break;
        }
    }

    private void parseNodeValue(SilkNodeBuilder node) throws XerialException {
        // ':' (<QName> | <NodeValue> | <String> | <Integer> | <Real>) 
        if (!nextTokenIs(SilkTokenType.Colon))
            return;

        consume();
        SilkToken t = LA(1);
        switch (t.type) {
        case QName:
        case NodeValue:
        case String:
        case Integer:
        case Real:
            consume();
            node.setValue(t.getText());
            break;
        }
    }

    private void parseAttributes(SilkNodeBuilder node) throws XerialException {
        SilkToken t = LA(1);
        switch (t.type) {
        case LParen:
            consume();
            parseAttributeItem(node);
            testAndConsume(SilkTokenType.RParen);
            break;
        case Separator:
            consume();
            parseAttributeItem(node);
            break;
        default:
            return;
        }
    }

    private void parseAttributeItem(SilkNodeBuilder node) throws XerialException {

        // <attribute> (',' <attribute>)*

        // <attribute name> <attribute type> <attribute value> 
        SilkNodeBuilder attribute = parseNode();
        node.addSilkNode(attribute);

        while (nextTokenIs(SilkTokenType.Comma)) {
            consume();
            attribute = parseNode();
            node.addSilkNode(attribute);
        }
    }

    private void parseNodeType(SilkNodeBuilder node) throws XerialException {

        // '<' <QName> '>'
        if (!nextTokenIs(SilkTokenType.LSquare))
            return;

        consume();
        CharSequence typeName = testAndConsume(SilkTokenType.QName).getText();
        node.setDataType(typeName);
        testAndConsume(SilkTokenType.RSquare);
    }

    /**
     * @param tokenType
     * @return true when the next token has the specified token type, otherwise
     *         false
     * @throws XerialException
     */
    private boolean nextTokenIs(SilkTokenType tokenType) throws XerialException {
        return LA(1).type == tokenType;
    }

    private SilkToken testAndConsume(SilkTokenType tokenType) throws XerialException {
        SilkToken t = LA(1);
        if (t.type == tokenType) {
            consume();
            return t;
        }
        else {
            throw unexpectedToken(t, tokenType);
        }
    }

    protected void parseNodeName(SilkNodeBuilder node) throws XerialException {
        for (;;) {
            SilkToken t = LA(1);
            switch (t.type) {
            case QName:
            case String:
                consume();
                node.setName(t.getText());
                return;
            case At:
                consume();
                node.setNodeType(SilkNodeType.FUNCTION);
                continue;
            default:
                return;
            }
        }
    }

    private XerialException unexpectedToken(SilkToken t, SilkTokenType... expectedTokenTypes) {
        String tokenNames = StringUtil.join(expectedTokenTypes, " or ");
        return new XerialException(XerialErrorCode.PARSE_ERROR, String.format("expected %s, but %s", tokenNames, t));
    }

}
